﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/


using System;
using System.Collections.Generic;
using Tesla.Bounding;
using Tesla.Collections;
using Tesla.Content;
using Tesla.Core;
using Tesla.Graphics;
using Tesla.Math;

namespace Tesla.Scene {
    /// <summary>
    /// The scene graph is a tree hierarchy that contains:
    /// 
    /// 1. Transformations
    /// 2. Bounding Volumes
    /// 3. Lights
    /// 
    /// Spatial is the root class that every object in the scene graph inherits from. A spatial is simply
    /// an entity that exists somewhere in the world, whether it's logical or a renderable model. Spatial's that
    /// are nodes can have children, those that are Mesh's represent a renderable 3D model and are "leaves" on the scene graph.
    /// 
    /// In the scene local vs world properties correspond to the frame - local values are local to the Spatial, where
    /// as world properties are absolute (and often are influenced by parents or children of the Spatial). These properties
    /// are updated when Update() is called. Spatial is smart enough to only recompute these values if they have been changed,
    /// reducing redundant computations.
    /// 
    /// During drawing operations, the scene graph automatically performs a culling check with the renderer's current camera. This
    /// is done in the OnDraw() method.
    /// </summary>
    public abstract class Spatial : ISavable, IHintable {
        private String _name;
        private Node _parent;
        private DirtyMark _dirtyMark;
        private Transform _localTransform;
        private Transform _worldTransform;
        private BoundingVolume _worldBounding;
        private TeslaList<ISpatialController> _controllers;
        private SceneHints _sceneHints;
        private ContainmentType _frustumIntersect = ContainmentType.Intersects;
        private LightCollection _localLights;

        /// <summary>
        /// Get or set this Spatial's name.
        /// </summary>
        public String Name {
            get {
                return _name;
            }
            set {
                _name = value;
            }
        }

        /// <summary>
        /// Gets this Spatial's parent. 
        /// </summary>
        public Node Parent {
            get {
                return _parent;
            }
            internal set {
                _parent = value;
            }
        }

        /// <summary>
        /// Get or set this Spatial's local transform.
        /// </summary>
        public Transform Transform {
            get {
                return _localTransform;
            }
            set {
                _localTransform = value;
                PropagateDirtyDown(DirtyMark.Transform | DirtyMark.Bounding | DirtyMark.Lighting);
                PropagateDirtyUp(DirtyMark.Bounding);
            }
        }

        /// <summary>
        /// Get or set this Spatial's local scale.
        /// </summary>
        public Vector3 Scale {
            get {
                return _localTransform.Scale;
            }
            set {
                _localTransform.Scale = value;
                PropagateDirtyDown(DirtyMark.Transform | DirtyMark.Bounding | DirtyMark.Lighting);
                PropagateDirtyUp(DirtyMark.Bounding);
            }
        }

        /// <summary>
        /// Get or set this Spatial's local rotation.
        /// </summary>
        public Quaternion Rotation {
            get {
                return _localTransform.Rotation;
            }
            set {
                _localTransform.Rotation = value;
                PropagateDirtyDown(DirtyMark.Transform | DirtyMark.Bounding | DirtyMark.Lighting);
                PropagateDirtyUp(DirtyMark.Bounding);
            }
        }

        /// <summary>
        /// Get or set this Spatial's local translation.
        /// </summary>
        public Vector3 Translation {
            get {
                return _localTransform.Translation;
            }
            set {
                _localTransform.Translation = value;
                PropagateDirtyDown(DirtyMark.Transform | DirtyMark.Bounding | DirtyMark.Lighting);
                PropagateDirtyUp(DirtyMark.Bounding);
            }
        }

        /// <summary>
        /// Gets this Spatial's world transform.
        /// </summary>
        public Transform WorldTransform {
            get {
                return _worldTransform;
            }
        }

        /// <summary>
        /// Gets this Spatial's world scale.
        /// </summary>
        public Vector3 WorldScale {
            get {
                return _worldTransform.Scale;
            }
        }

        /// <summary>
        /// Get this Spatial's world rotation.
        /// </summary>
        public Quaternion WorldRotation {
            get {
                return _worldTransform.Rotation;
            }
        }

        /// <summary>
        /// Get this Spatial's world translation.
        /// </summary>
        public Vector3 WorldTranslation {
            get {
                return _worldTransform.Translation;
            }
        }

        /// <summary>
        /// Get the world matrix for this Spatial. This is equivalent to
        /// spatial.WorldTransforms.GetMatrix()
        /// </summary>
        public Matrix WorldMatrix {
            get {
                return _worldTransform.Matrix;
            }
        }

        /// <summary>
        /// Gets or sets the world bounding for this Spatial. This may be null.
        /// </summary>
        public BoundingVolume WorldBounding {
            get {
                return _worldBounding;
            }
            protected internal set {
                _worldBounding = value;
            }
        }

        /// <summary>
        /// Gets the read-only controller collection attached to this Spatial.
        /// </summary>
        public ReadOnlyList<ISpatialController> Controllers {
            get {
                return _controllers.AsReadOnly();
            }
        }

        /// <summary>
        /// Gets the IHintable parent fro this Spatial, for inheritable scene hints to use.
        /// </summary>
        public IHintable ParentHintable {
            get {
                return _parent;
            }
        }

        /// <summary>
        /// Gets this Spatial's scene hints, a collection of enumerations
        /// that describe how the spatial should be processed.
        /// </summary>
        public SceneHints SceneHints {
            get {
                return _sceneHints;
            }
        }

        /// <summary>
        /// Gets or sets the last frustum intersection result, which is used in the cull check in OnDraw(). Set this
        /// if you render a subtree and do not want it to be influenced by the parent.
        /// </summary>
        public ContainmentType LastFrustumIntersect {
            get {
                return _frustumIntersect;
            }
            set {
                _frustumIntersect = value;
            }
        }

        /// <summary>
        /// Gets the local light collection relative to this Spatial. This collection is not read-only, but it is safe to modify
        /// its content as it will update the spatial it is attached to automatically.
        /// </summary>
        public ILightCollection Lights {
            get {
                return _localLights;
            }
        }

        /// <summary>
        /// Do not use directly, for ISavable
        /// </summary>
        protected Spatial() {}

        /// <summary>
        /// Creates a new spatial with the specified name.
        /// </summary>
        /// <param name="name"></param>
        public Spatial(String name) {
            _name = name;
            _parent = null;
            _localTransform = new Transform();
            _worldTransform = new Transform();
            _controllers = new TeslaList<ISpatialController>(1);
            _dirtyMark = DirtyMark.Bounding | DirtyMark.Transform | DirtyMark.Lighting;
            _sceneHints = new SceneHints(this);
            _localLights = new LightCollection(this);
        }

        /// <summary>
        /// Accept a visitor and let it perform an action on this Spatial.
        /// </summary>
        /// <param name="visitor">Visitor</param>
        /// <param name="preexecute">If the visitor should be executed before it visits children (breadth vs depth)</param>
        public virtual void AcceptVisitor(IVisitor visitor, bool preexecute) {
            visitor.Visit(this);
        }

        /// <summary>
        /// Add a controller to this Spatial's collection.
        /// </summary>
        /// <param name="controller">Controller to add</param>
        public void AddController(ISpatialController controller){
            _controllers.Add(controller);
        }

        /// <summary>
        /// Gets the controller at the specified index in this Spatial's collection. If
        /// the index is out of range, null will be returned.
        /// </summary>
        /// <param name="index">Index of the controller</param>
        /// <returns>The controller or null if doesn't exist or the index was invalid</returns>
        public ISpatialController GetControllerAt(int index) {
            if(index >= _controllers.Count || index < 0) {
                return null;
            }
            return _controllers[index];
        }

        /// <summary>
        /// Removes a controller from this Spatial's collection.
        /// </summary>
        /// <param name="controller">Controller to remove</param>
        /// <returns>True if operation was successful</returns>
        public bool RemoveController(ISpatialController controller){
            return _controllers.Remove(controller);
        }

        /// <summary>
        /// Removes a controller from the specified index in this Spatial's collection. If the index
        /// is out of range, then false will be returned.
        /// </summary>
        /// <param name="index">Index to remove controller at</param>
        /// <returns>True if operation was successful</returns>
        public bool RemoveControllerAt(int index){
            if(index >= _controllers.Count || index < 0) {
                return false;
            }

            ISpatialController c = _controllers[index];

            if(c != null) {
                _controllers.RemoveAt(index);
                return true;
            }
            return false;
        }

        /// <summary>
        /// Remove all controllers from this Spatial's collection.
        /// </summary>
        public void RemoveAllControllers() {
            _controllers.Clear();
        }

        /// <summary>
        /// Marks this spatial with the dirty flag and then propagates
        /// it up the scene graph.
        /// </summary>
        /// <param name="flag">DirtyMark bit flag</param>
        public void PropagateDirtyUp(DirtyMark flag) {
            _dirtyMark |= flag;
            if(_parent != null) {
                _parent.PropagateDirtyUp(flag);
            }
        }

        /// <summary>
        /// Marks this spatial with the dirty flag and then propagates it down the
        /// scene graph. This is meant to be overridden by subclasses that have children.
        /// </summary>
        /// <param name="flag">DirtyMark bit flag</param>
        public virtual void PropagateDirtyDown(DirtyMark flag) {
            _dirtyMark |= flag;
        }

        /// <summary>
        /// Checks if this Spatial is marked with the
        /// specified dirty mark.
        /// </summary>
        /// <param name="flag">DirtyMark bit flag</param>
        /// <returns>True if the spatial is marked with the flag</returns>
        public bool IsDirty(DirtyMark flag) {
            return (_dirtyMark & flag) == flag;
        }

        /// <summary>
        /// Marks the spatial dirty, but does not propagate the mark up/down the tree.
        /// </summary>
        /// <param name="flag">DirtyMark bit flag</param>
        public void MarkDirty(DirtyMark flag) {
            _dirtyMark |= flag;
        }

        /// <summary>
        /// Clears the specified dirty mark from this Spatial.
        /// </summary>
        /// <param name="flag">DirtyMark bit flag</param>
        public void ClearDirty(DirtyMark flag) {
            _dirtyMark &= ~flag;
        }

        /// <summary>
        /// Removes this spatial from its parent, if it exists.
        /// </summary>
        /// <returns>If the operation was a success</returns>
        public bool RemoveFromParent() {
            if(_parent != null) {
                _parent.RemoveChild(this);
                _parent = null;
                return true;
            }
            return false;
        }

        /// <summary>
        /// Convience method to set local uniform scale.
        /// </summary>
        /// <param name="scale">Scale for all three axes.</param>
        public void SetScale(float scale) {
            Scale = new Vector3(scale, scale, scale);
        }

        /// <summary>
        /// Convience method to set local translation.
        /// </summary>
        /// <param name="x">X coordinate</param>
        /// <param name="y">Y coordinate</param>
        /// <param name="z">Z coordinate</param>
        public void SetTranslation(float x, float y, float z) {
            Translation = new Vector3(x, y, z);
        }

        /// <summary>
        /// Convience method to set local rotation from a matrix.
        /// </summary>
        /// <param name="rotation">Rotation matrix</param>
        public void SetRotation(Matrix rotation) {
            _localTransform.SetRotation(rotation);
            PropagateDirtyDown(DirtyMark.Transform | DirtyMark.Bounding);
            PropagateDirtyUp(DirtyMark.Bounding);
        }

        /// <summary>
        /// Spatial's draw method. Used by subclasses: if a Node, it
        /// calls OnDraw on each child, if a mesh then it would render itself
        /// or be assigned to the render queue.
        /// </summary>
        /// <param name="renderer">BaseRenderer used to draw</param>
        public abstract void Draw(IRenderer renderer);

        /// <summary>
        /// Checks if the Spatial should be culled, if the Spatial is
        /// visible then Draw() will be called.
        /// </summary>
        /// <param name="renderer">BaseRenderer used to draw</param>
        public void OnDraw(IRenderer renderer) {
            CullHint hint = _sceneHints.CullHint;

            //Do trivial check
            if(hint == CullHint.Always) {
                _frustumIntersect = ContainmentType.Outside;
                return;
            } else  if(hint == CullHint.Never) {
                _frustumIntersect = ContainmentType.Intersects;
                Draw(renderer);
                return;
            }

            //If dynamic, now check the frustum
            ICamera cam = renderer.CurrentCamera;

            //Set what our parent was, if any
            if(Parent != null) {
                _frustumIntersect = _parent.LastFrustumIntersect;
            } else {
                _frustumIntersect = ContainmentType.Intersects;
            }

            //If our parent intersects with the frustum, we need to check this Spatial since it may
            //be outside. If the parent was inside, then we are guaranteed to be contained as well.
            if(hint == CullHint.Dynamic && _frustumIntersect == ContainmentType.Intersects) {
                _frustumIntersect = cam.Contains(_worldBounding);
            }

            //If intersecting/inside, draw
            if(_frustumIntersect != ContainmentType.Outside) {
                Draw(renderer);
            }
        }

        /// <summary>
        /// Update the Spatial. Updating it will update the transformation, lighting, and bounding properties
        /// if they are dirty.
        /// 
        /// Update order:
        /// 
        /// 1. Controllers
        /// 2. World Transform
        /// 3. World lighting
        /// 4. Children (if Node)
        /// 5. World Bounding
        /// 6. Propagate Bound (if initiator)
        /// </summary>
        /// <param name="time">Interpolation time</param>
        public void Update(GameTime time) {
            Update(time, true);
        }

        /// <summary>
        /// Update the Spatial. Updating it will update the transformation, lighting, and bounding properties
        /// if they are dirty. This is mainly used internally, as initiating will propagate the scene world bounding to the root node,
        /// after its children and their children are updated (if a Node).
        /// 
        /// Update order:
        /// 
        /// 1. Controllers
        /// 2. World Transform
        /// 3. World lighting
        /// 4. Children (if Node)
        /// 5. World Bounding
        /// 6. Propagate Bound (if initiator)
        /// </summary>
        /// <param name="time">Interpolation time</param>
        /// <param name="initiator">True if this node is the initiator (usually the root)</param>
        public virtual void Update(GameTime time, bool initiator) {
            UpdateControllers(time);

            if(IsDirty(DirtyMark.Transform)) {
                UpdateWorldTransform(false);
            }

            //TODO: Update lighting
            if(IsDirty(DirtyMark.Lighting)) {
                UpdateWorldLights(false);
            }
            
            if(IsDirty(DirtyMark.Bounding)) {
                UpdateWorldBound(false);
                if(initiator) {
                    PropagateBoundToRoot();
                }
            }
        }

        /// <summary>
        /// Updates the controllers attached to this node.
        /// </summary>
        /// <param name="time">Interpolation</param>
        public void UpdateControllers(GameTime time) {
            for(int i = 0; i < _controllers.Count; i++) {
                ISpatialController c = _controllers[i];
                if(c.IsEnabled) {
                    c.Update(time, this);
                }
            }
        }

        /// <summary>
        /// Updates the world transform of this spatial, clearing the dirty mark if present.
        /// </summary>
        /// <param name="recurse">True to recursively update the children's world transform if this is a Node</param>
        public virtual void UpdateWorldTransform(bool recurse) {
            if(_parent != null) {
                _worldTransform.Set(_localTransform);
                _worldTransform.CombineWithParent(_parent.WorldTransform);
            } else {
                _worldTransform.Set(_localTransform);
            }
            //Cause the world matrix to be computed, then cached.
            Matrix m = _worldTransform.Matrix;
            ClearDirty(DirtyMark.Transform);
        }

        /// <summary>
        /// Updates the world bounding of this spatial. If a node, it is a bounding volume that contains
        /// all of the volumes of its children (and their children). The volume type is dictated by the first
        /// valid child world bounding. If it's a mesh, it is the transformed model bound. 
        /// </summary>
        /// <param name="recurse">True to recursively update the children's world bounding if this is a Node</param>
        public abstract void UpdateWorldBound(bool recurse);

        /// <summary>
        /// Update this Spatial's parent world bounding and propagates the call to the root, ensuring all ancestors' world
        /// boundings properly contain their children's.
        /// </summary>
        public void PropagateBoundToRoot() {
            if(_parent != null) {
                _parent.UpdateWorldBound(false);
                _parent.PropagateBoundToRoot();
            }
        }

        /// <summary>
        /// Convienence method to propagate a render state down the scene graph.
        /// </summary>
        /// <param name="renderState">Renderstate</param>
        public abstract void SetRenderState(RenderState renderState);

        /// <summary>
        /// Convienence method to propagate a model bounding volume down the scene graph.
        /// </summary>
        /// <param name="volume">Bounding Volume</param>
        public abstract void SetModelBound(BoundingVolume volume);

        /// <summary>
        /// Convienence method to propagate a material down the scene graph.
        /// </summary>
        /// <param name="material">Material</param>
        public abstract void SetMaterial(Material material);

        /// <summary>
        /// Adds a light to the Spatial's light collection.
        /// </summary>
        /// <param name="light"></param>
        public void AddLight(Light light) {
            //Note: LightCollection will propagate the lighting dirty mark down for us
             _localLights.Add(light);
        }

        /// <summary>
        /// Gets the light from the Spatial's light collection if it exists.
        /// </summary>
        /// <param name="index">Index of the light in the collection</param>
        /// <returns>The light, or null if not present or index was invalid</returns>
        public Light GetLightAt(int index) {
            if(index >= _localLights.LightCount || index < 0) {
                return null;
            }
            return _localLights[index];
        }

        /// <summary>
        /// Removes the specified light from the Spatial light collection, if it exists.
        /// </summary>
        /// <param name="light">Light to remove</param>
        /// <returns>True if succeeds</returns>
        public bool RemoveLight(Light light) {
            //Note: LightCollection will propagate the lighting dirty mark down for us
            return _localLights.Remove(light);
        }

        /// <summary>
        /// Removes a light by index from the Spatial's light collection.
        /// </summary>
        /// <param name="index">Light index</param>
        /// <returns>True if succeeds</returns>
        public bool RemoveLightAt(int index) {
            if(index >= _localLights.LightCount || index < 0) {
                return false;
            }
            //Note: LightCollection will propagate the lighting dirty mark down for us
            return _localLights.RemoveAt(index);
        }

        /// <summary>
        /// Removes all lights from the Spatial's light collection.
        /// </summary>
        public void RemoveAllLights() {
            //Note: LightCollection will propagate the lighting dirty mark down for us
            _localLights.Clear();
        }

        /// <summary>
        /// Sorts world lights.
        /// </summary>
        public abstract void SortLights();

        /// <summary>
        /// Updates world lights.
        /// </summary>
        /// <param name="recurse">True if children should be updated</param>
        public abstract void UpdateWorldLights(bool recurse);

        /// <summary>
        /// Collects all the lights from this Spatial up to the root, copying all lights at each node.
        /// </summary>
        /// <param name="lights">Light collection that will contain the copied lights</param>
        protected void CollectLights(LightCollection lights) {
            lights.CopyLights(_localLights);

            if(_parent != null) {
                _parent.CollectLights(lights);
            }
        }

        /// <summary>
        /// Serializes the object and writes it to the output.
        /// </summary>
        /// <param name="output">Savable Output</param>
        public virtual void Write(ISavableWriter output) {
            output.Write("Name", _name);
            output.WriteSavable<Transform>("LocalTransform", _localTransform);
            output.WriteSavable<SceneHints>("SceneHints", _sceneHints);
            output.WriteSavable<LightCollection>("LocalLights", _localLights);

            List<ISavable> savableControllers = new List<ISavable>();

            for(int i = 0; i < _controllers.Count; i++) {
                ISpatialController controller = _controllers[i];
                if(controller is ISavable) {
                    savableControllers.Add(controller as ISavable);
                }
            }

            if(savableControllers.Count > 0) {
                output.Write("HasControllers", true);
                output.WriteSavable<ISavable>("Controllers", savableControllers.ToArray());
            } else {
                output.Write("HasControllers", false);
            }
        }


        /// <summary>
        /// Deserializes the object and populates it from the input.
        /// </summary>
        /// <param name="input">Savable input</param>
        public virtual void Read(ISavableReader input) {
            _name = input.ReadString();
            _localTransform = input.ReadSavable<Transform>();
            _sceneHints = input.ReadSavable<SceneHints>();
            _sceneHints.HintableSource = this;
            _localLights = input.ReadSavable<LightCollection>();
            _localLights._spatial = this;

            bool hasControllers = input.ReadBoolean();
            _controllers = new TeslaList<ISpatialController>(1);

            if(hasControllers) {
                ISavable[] savableControllers = input.ReadSavableArray<ISavable>();
                for(int i = 0; i < savableControllers.Length; i++) {
                    ISpatialController controller = savableControllers[i] as ISpatialController;
                    if(controller != null) {
                        _controllers.Add(controller);
                    }
                }
            }

            _worldTransform = new Transform(_localTransform);
            _frustumIntersect = ContainmentType.Intersects;
            _parent = null;
            _dirtyMark = DirtyMark.Bounding | DirtyMark.Transform | DirtyMark.Lighting;
        }
    }
}
